import time
import random
import traceback

import pandas as pd
import numpy as np
import redis
from lightfm import LightFM
from lightfm.data import Dataset
from sqlalchemy import create_engine, text
from pymongo import MongoClient
from bson import ObjectId
import sys
from pathlib import Path

# 把项目根加入到 sys.path
sys.path.append(str(Path(__file__).parent.parent))

from utils.config_loader import load_settings
from utils.db import get_connection


# ─── 配置 ──────────────────────────────────────────
settings = load_settings()

# MySQL 连接（user_behavior 存储在 MySQL）
MYSQL_URI = settings['database']['uri']

# MongoDB 连接（商品元数据存储在 MongoDB）
MONGO_URI = settings['mongodb']['uri']
MONGO_DB  = settings['mongodb']['database']

# Redis 连接（用于缓存召回结果）
redis_cfg = settings['redis']
host_str = redis_cfg['host']
if "@" in host_str:
    REDIS_HOST, port_str = host_str.split("@", 1)
    REDIS_PORT = int(port_str)
else:
    REDIS_HOST = host_str
    REDIS_PORT = redis_cfg['port']
REDIS_DB      = redis_cfg['db']
DEFAULT_TTL   = redis_cfg.get('default_ttl', 86400)

# ─── 初始化客户端 ──────────────────────────────────
# MySQL SQLAlchemy 引擎
engine = create_engine(MYSQL_URI)

# Redis 客户端
try:
    _r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, decode_responses=True)
    _r.ping()
    print(f"[DEBUG] ✅ Redis 已连接: {REDIS_HOST}:{REDIS_PORT}, db={REDIS_DB}")
except Exception as e:
    print(f"[ERROR] ❌ Redis 连接失败: {e}")
    exit(1)

# MongoDB 客户端
_mongo_client = MongoClient(MONGO_URI)
_mongo_db     = _mongo_client[MONGO_DB]


# ─── STEP 1: 加载用户行为数据 (MySQL) ───────────────
def load_behavior_data() -> pd.DataFrame:
    """
    从 MySQL 的 user_behavior 表加载用户—商品行为，
    并映射为 LightFM 可用的 (user_id, product_id, weight) DataFrame。
    """
    print("[DEBUG] 加载用户行为数据...")
    sql = """
    SELECT user_id, product_id, behavior_type
    FROM user_behavior
    WHERE behavior_type IN ('view', 'collect', 'buy')
    """
    df = pd.read_sql(text(sql), engine)
    print(f"[DEBUG] 执行 SQL，读取 {len(df)} 条行为记录")
    if not df.empty:
        print(f"用户行为数据示例:\n{df.head()}")
    else:
        print("[ERROR] 用户行为数据为空")

    # 行为权重映射
    mapping = {'view': 1.0, 'collect': 3.0, 'buy': 5.0}
    df['weight'] = df['behavior_type'].map(mapping).fillna(0)
    return df[['user_id', 'product_id', 'weight']]


# ─── STEP 2: 加载商品元数据 (MongoDB) ───────────────
def load_product_info():
    """
    从 MongoDB 读取每个商品的分类信息，并构建两张 map:
      1) product_category_map: { product_id: category }
      2) product_collection_map: { product_id: collection_name }
    用于后续同类商品扩展召回。
    """
    print("[DEBUG] 加载商品元数据：分类映射")
    product_category_map    = {}
    product_collection_map  = {}

    for coll_name in _mongo_db.list_collection_names():
        if coll_name == "categories":
            continue
        coll = _mongo_db[coll_name]
        cursor = coll.find({}, {"_id": 1, "category": 1})
        for doc in cursor:
            pid = str(doc["_id"])
            product_category_map[pid]   = doc.get("category", "unknown")
            product_collection_map[pid] = coll_name

    print(f"[DEBUG] 商品总数 (含分类信息): {len(product_category_map)}")
    return product_category_map, product_collection_map


# ─── STEP 3: 构建 LightFM Dataset ─────────────────
def build_dataset(df: pd.DataFrame):
    """
    构建 LightFM 所需的 Dataset、interactions 矩阵。
    """
    print("[DEBUG] 构建 LightFM Dataset...")
    ds = Dataset()
    ds.fit(
        users=df['user_id'].unique(),
        items=df['product_id'].unique()
    )
    interactions, weights = ds.build_interactions(
        ((row['user_id'], row['product_id'], row['weight'])
         for _, row in df.iterrows())
    )
    print(f"[DEBUG] Dataset: users={ds.interactions_shape()[0]}, items={ds.interactions_shape()[1]}")
    print(f"[DEBUG] 检查 interactions 矩阵:")
    print(f"  类型: {type(interactions)}")
    print(f"  形状: {interactions.shape}")
    print(f"  非零元素数量: {interactions.nnz}")
    return ds, interactions


# ─── STEP 4: 训练 LightFM 模型 ─────────────────────
def train_model(interactions):
    print("[调试] 训练 LightFM 模型...")
    start = time.time()
    model = LightFM(loss='warp')
    model.fit(interactions, epochs=1, num_threads=1)
    print(f"[调试] 模型训练完成，耗时 {time.time()-start:.2f} 秒")
    return model


# ─── STEP 5: 同类商品扩展函数 ─────────────────────
def expand_similar_items(item_ids, product_category_map, product_collection_map, top_k=5):
    """
    对一组已召回商品，按其分类从 MongoDB 中额外查询同类商品。
    返回同类商品 ID 列表（去重）。
    """
    sim_items = set()
    for pid in item_ids:
        coll_name = product_collection_map.get(pid)
        cat       = product_category_map.get(pid)
        if not coll_name or not cat:
            print(f"[警告] 商品 {pid} 缺少分类或集合信息，跳过同类扩展")
            continue
        coll = _mongo_db[coll_name]
        cursor = coll.find(
            {"category": cat, "_id": {"$ne": ObjectId(pid)}},
            {"_id": 1}
        ).limit(top_k)
        print(f"为商品 {pid} 查询到的同类商品: {[str(doc['_id']) for doc in cursor]}")
        for doc in cursor:
            sim_items.add(str(doc['_id']))
    return list(sim_items)


# ─── STEP 6: 生成召回并写入 Redis ─────────────────
def generate_recommendations(model, ds, df_beh, product_category_map, product_collection_map, top_n=10):
    """
    针对所有用户：
      1) 用 LightFM 召回 Top-N
      2) 扩展同类商品
      3) 写入 Redis （按 recall:{user_id}:{category} 存有序集合）
    """
    print("[调试] 生成召回推荐...")
    _, _, user_map, item_map = ds.mapping()
    all_users  = df_beh['user_id'].unique()

    for uid in all_users:
        try:
            uidx = user_map.get(uid)
            if uidx is None:
                print(f"[警告] 用户 {uid} 未在 LightFM 中找到，跳过")
                continue

            # 1) LightFM 召回
            scores = model.predict(uidx, np.arange(ds.interactions_shape()[1]))
            print(f"用户 {uid} 的预测分数: {scores}")
            top_idxs = np.argsort(-scores)[:top_n]
            recs = [(item_map[i], float(scores[i])) for i in top_idxs]

            # 2) 同类商品扩展
            orig_ids      = [pid for pid, _ in recs]
            sim_ids       = expand_similar_items(orig_ids, product_category_map, product_collection_map, top_k=5)
            # 将扩展商品也写入，给个较低分
            if recs:
                min_score = min(s for _, s in recs)
            else:
                min_score = 1.0
            sim_score = min_score * 0.5
            recs.extend([(pid, sim_score) for pid in sim_ids if pid not in orig_ids])

            # 3) 写入 Redis：有序集合，按分类分Key
            pipe = _r.pipeline()
            for pid, score in recs:
                cat = product_category_map.get(pid, 'unknown')
                key = f"recall:{uid}:{cat}"
                pipe.zadd(key, {pid: score})
                pipe.expire(key, DEFAULT_TTL)
            pipe.execute()

            print(f"[调试] 用户 {uid} 共写入 {len(recs)} 条召回（含同类扩展）")

        except Exception:
            print(f"[错误] 用户 {uid} 召回异常:")
            traceback.print_exc()


# ─── STEP 7: 从 Redis 获取最终推荐 ─────────────────
def get_user_recommendations(uid, top_n=20):
    """
    合并 Redis 中 recall:{uid}:* 的所有分类有序集，返回 Top-n 商品 ID。
    """
    print(f"[调试] 获取用户 {uid} 的最终推荐...")
    keys = _r.keys(f"recall:{uid}:*")
    if not keys:
        print(f"[警告] 用户 {uid} 没有推荐结果")
        return []
    out_key = f"temp:recall:{uid}"
    _r.zunionstore(out_key, keys)
    recs = _r.zrevrange(out_key, 0, top_n - 1)
    _r.expire(out_key, DEFAULT_TTL)
    print(f"用户 {uid} 的最终推荐: {recs}")
    return recs


# ─── 主流程 ───────────────────────────────────────
if __name__ == "__main__":
    try:
        # 1) 行为数据
        df_beh = load_behavior_data()
        if df_beh.empty:
            print("[错误] 无行为数据，退出")
            exit(1)
        print(f"用户行为数据示例:\n{df_beh.head()}")

        # 2) 商品元数据映射
        product_category_map, product_collection_map = load_product_info()
        print(f"商品分类映射示例: {list(product_category_map.items())[:5]}")

        # 3) 构建 & 训练模型
        ds, interactions = build_dataset(df_beh)
        model = train_model(interactions)

        # 4) 生成召回 + 同类扩展
        generate_recommendations(
            model, ds, df_beh,
            product_category_map, product_collection_map,
            top_n=10
        )

        # 5) 测试输出某用户结果
        sample_u = random.choice(df_beh['user_id'].unique())
        print(f"[结果] 用户 {sample_u} Top-10 推荐：{get_user_recommendations(sample_u, top_n=10)}")

    except Exception as e:
        print(f"[错误] 主程序异常: {e}")
        traceback.print_exc()